Skip to content

feat(backend): implement analytics plan-statistics endpoint - #1055

Merged
ONEONUORA merged 2 commits into
Fracverse:masterfrom
TheWeirdDee:feat/1036-plan-statistics-endpoint
Aug 26, 2026
Merged

feat(backend): implement analytics plan-statistics endpoint#1055
ONEONUORA merged 2 commits into
Fracverse:masterfrom
TheWeirdDee:feat/1036-plan-statistics-endpoint

Conversation

@TheWeirdDee

Copy link
Copy Markdown
Contributor

Summary

Implements the missing GET /api/analytics/plan-statistics endpoint that the Admin Dashboard already calls via PlansAPI.getPlanStatistics() (frontend/app/lib/api/plans.ts), which previously 404'd because no Axum route handler existed for it.

Closes #1036

What's in this PR

backend/src/api.rs

  • New GET /api/analytics/plan-statistics route, registered on the admin_routes router and protected by jwt_auth_middleware (admin-role JWT only), exactly as the issue specifies.
  • New PlanStatisticsQuery extractor with three optional filters — start_date, end_date (both RFC 3339 timestamps, filtered against plans.created_at), and asset_type (filtered against plans.token_address) — all applied consistently across every metric in the response.
  • The handler aggregates, in three queries built with sqlx::QueryBuilder to keep the dynamic filters injection-safe:
    • Summary counts: total_plans, active_plans, expired_plans, triggered_plans, claimed_plans, computed with COUNT(*) FILTER (WHERE ...) in a single round trip.
    • by_status: a GROUP BY status breakdown, for the dashboard's status chart.
    • locked_value_by_asset: SUM(amount) and plan count grouped by token_address, restricted to is_active = true plans, i.e. this is the "total value locked" the issue asks for, broken out per asset currency.
  • Status-bucket definitions reuse vocabulary that already exists in the codebase rather than inventing new ones:
    • active_plans = status = 'ACTIVE' and not yet past inactivity_deadline_at.
    • expired_plans = status = 'ACTIVE' and past inactivity_deadline_at — this mirrors the ExpiredPlan concept the inactivity watchdog (backend/src/inactivity_watchdog.rs) already uses for plans that are due for a sweep but haven't been picked up yet.
    • triggered_plans = status IN ('TRIGGERING', 'TRIGGERED', 'TRIGGER_FAILED').
    • claimed_plans = status IN ('CLAIMABLE', 'PAID_OUT') (set by the claim flow / payout pipeline).
  • Returns 400 if start_date > end_date.
  • Response is wrapped as { "data": ... }, consistent with the other newer plan endpoints in this file (claim_plan, cancel_plan, etc.).

backend/src/cache.rs

  • Adds generic get_stats<T>() / set_stats<T>() methods to PlanCache (backed by new private get_raw/set_raw helpers), so any JSON-serializable value can be cached through the same Disabled / Redis / in-memory-fallback enum the plan-list cache already uses — without reusing the plan-list's query-index/invalidation machinery, which doesn't apply here.
  • Unlike the plan-list cache (which uses one TTL from RedisPlanCache), set_stats takes an explicit per-call TTL, so the statistics cache can have its own, independently configurable TTL.
  • Adds plan_statistics_cache_key() to build a normalized cache key from the query filters.
  • New TTL is configurable via PLAN_STATISTICS_CACHE_TTL_SECS (default 60s), read in backend/src/config.rs and threaded onto AppState — deliberately separate from PLAN_CACHE_TTL_SECS (default 15s) since this query aggregates the whole plans table and is far more expensive than a single plan lookup, and this endpoint is only ever hit by the admin dashboard, so a longer staleness window is fine. No cache invalidation hooks were added on plan mutations; the short TTL is what bounds staleness here, matching the issue's ask ("prevent database load spikes on admin dashboard refresh").
  • Added unit tests for the generic cache round-trip and for cache-key normalization.

frontend/app/lib/api/plans.ts

  • Extended PlanStatistics with locked_value_by_asset: AssetLockedValue[] (new exported type) to match the new response shape.
  • getPlanStatistics() now accepts an optional PlanStatisticsFilters argument (startDate / endDate / assetType) and forwards them as query params.
  • Re-exported the two new types from frontend/app/lib/api/index.ts.

backend/.env.example

  • Documented the new PLAN_STATISTICS_CACHE_TTL_SECS variable.

Why these design choices

  • The issue's "Affected Files" list (backend/src/api.rs, frontend/app/lib/api/plans.ts, backend/src/cache.rs) is exactly what's touched here — no unrelated files or refactors.
  • Reused the existing PlanCache enum (Redis-or-memory-or-disabled) instead of introducing a second cache type, since the issue explicitly names backend/src/cache.rs as the place for this.
  • Chose sqlx::QueryBuilder over hand-rolled string concatenation for the optional filters so date/asset-type values are always parameter-bound, never interpolated into SQL.
  • No new database migration was needed — every column this endpoint reads (status, created_at, token_address, amount, is_active, inactivity_deadline_at) already exists on plans.

Test plan

  • Added backend/tests/api_tests.rs::test_plan_statistics_requires_auth — asserts the route 401s without a JWT (mirrors the existing test_freeze_loans_requires_auth-style auth-guard tests in that file).
  • Added backend/src/cache.rs unit tests: memory_cache_round_trips_generic_stats and plan_statistics_cache_keys_are_normalized_and_stable.
  • Updated the three existing AppState construction sites (backend/src/main.rs, backend/tests/api_tests.rs x2, backend/tests/kyc_webhook_test.rs) for the new plan_statistics_cache_ttl_secs field.
  • cargo build / cargo test — I wasn't able to run these in my environment (no Rust toolchain installed), so please run the full suite in CI before merging.
  • Manual check against a running admin JWT + seeded plans table to eyeball the aggregate numbers, since I couldn't spin up Postgres locally either.

Adds GET /api/analytics/plan-statistics, protected by jwt_auth_middleware,
so the admin dashboard's PlansAPI.getPlanStatistics() call has a handler
to hit instead of 404ing.
@drips-wave

drips-wave Bot commented Aug 25, 2026

Copy link
Copy Markdown

@TheWeirdDee Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@ONEONUORA

Copy link
Copy Markdown
Contributor

@TheWeirdDee
Make your implementation to pas CI checks

@ONEONUORA

Copy link
Copy Markdown
Contributor

@TheWeirdDee
Update on this issue

@TheWeirdDee

Copy link
Copy Markdown
Contributor Author

@TheWeirdDee

Update on this issue

I will soon, thank you

CI runs `cargo fmt --all -- --check`; a few lines in the new
plan-statistics code exceeded rustfmt line-width defaults.

@ONEONUORA ONEONUORA left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job @TheWeirdDee

@ONEONUORA
ONEONUORA merged commit 0a4caa2 into Fracverse:master Aug 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

backend: Implement Analytics & Admin Plan Statistics Endpoint

2 participants